package it.traveldream.beans;

import it.traveldream.dto.ExcursionDTO;
import it.traveldream.dto.HotelDTO;
import it.traveldream.dto.HotelDTO.HotelRoomType;
import it.traveldream.dto.LocationDTO;
import it.traveldream.dto.ReservationDTO;
import it.traveldream.dto.TransportMeanDTO;
import it.traveldream.dto.UserDTO;
import it.traveldream.entities.BookedExcursion;
import it.traveldream.entities.BookedHotel;
import it.traveldream.entities.BookedMean;
import it.traveldream.entities.Customer;
import it.traveldream.entities.CustomerInvites;
import it.traveldream.entities.Excursion;
import it.traveldream.entities.Hotel;
import it.traveldream.entities.Location;
import it.traveldream.entities.MeanType;
import it.traveldream.entities.Reservation;
import it.traveldream.entities.TransportMean;
import it.traveldream.exceptions.InvalidReservationException;
import it.traveldream.exceptions.LocationNotFoundException;
import it.traveldream.exceptions.ProductNotFoundException;
import it.traveldream.exceptions.ProductSoldOutException;
import it.traveldream.exceptions.ReservationNotFoundException;
import it.traveldream.utils.BookedMeanDescriptor;
import it.traveldream.utils.BookedRoomDescriptor;
import it.traveldream.utils.CalendarUtils;
import it.traveldream.utils.JavaBeanResult;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.annotation.Resource;
import javax.annotation.security.RolesAllowed;
import javax.ejb.EJBContext;
import javax.ejb.Local;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

/**
 * Session Bean implementation class ReservationManager
 */
@Stateless
@Local(BookingManagerInterface.class)
@LocalBean
@TransactionManagement(value=TransactionManagementType.BEAN)
public class ReservationManager implements BookingManagerInterface {

	@PersistenceContext
	private EntityManager em;
	private Logger log;
	
	@Resource
	private UserTransaction utx;
	
	@Resource
	private EJBContext context;
    /**
     * Default constructor. 
     */
    public ReservationManager() {
    	super();
        this.log = Logger.getLogger(this.getClass().getSimpleName());
    }

	/**
     * @see BookingManagerInterface#getAvailableHotels(LocationDTO, Date, Date, int)
     */
    @SuppressWarnings("unchecked")
    @Override
    @RolesAllowed({Customer.role})
    public List<HotelDTO> getAvailableHotels(LocationDTO location, Date fromDate, Date toDate, Map<HotelRoomType, Integer> rooms) {
    	try{
    		this.utx.begin();
    		try{
	    		Location l = this.em.find(Location.class, location.getLocationId());
	
	    		List<Hotel> hotels = this.em.createNamedQuery("getAllHotelInLocation").
						setParameter("location", l).
						getResultList();
	    		// Fetch previous bookings
	    		Query hotelBookings = this.em.createNamedQuery("getHotelsInLocationBookedRoomsBetweenDates").
	    				setParameter(1, l.getId()).
	    				setParameter(2, fromDate).
	    				setParameter(3, toDate);
				JavaBeanResult.setQueryResultClass(hotelBookings, BookedRoomDescriptor.class);
	    		List<BookedRoomDescriptor> bookings = hotelBookings.getResultList();
	    		
	    		// Processing bookings
	    		List<Hotel> available = new ArrayList<>();
	    		for (Hotel h : hotels){
	    			for (HotelRoomType type : rooms.keySet())
	    				if (h.getHotelRooms().get(type).getRooms() >= rooms.get(type))
	    					available.add(h);
	    		}
	    		Iterator<Hotel> i = available.iterator();
	    		while(i.hasNext()){
					Hotel h = i.next(); 
					for (BookedRoomDescriptor d : bookings){ // for each room type
						if (h.getProductId().equals(d.getHotelId())){ // right hotel
							// not enough available rooms?
							if ((d.getTotalRooms() - d.getBookedRooms()) < rooms.get(d.getType())){
								i.remove();
							}
						}
					}
				}
				
				
				this.utx.commit();
				List<HotelDTO> dto = new ArrayList<>();
				for (Hotel h : available)
					dto.add(h.toDTO());
				return dto;
					
		    	} catch (Exception e){
		    		this.utx.rollback();
					this.log.severe("ERROR: Fetching hotels: " + e.getMessage());
					throw new RuntimeException();
				}
    	} catch (Exception e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
			throw new RuntimeException();
    	}
    }

	/**
     * @see BookingManagerInterface#sendInvitationLink(ReservationDTO, UserDTO, String)
     */
    @Override
    @RolesAllowed({Customer.role})
    public String sendInvitationLink(ReservationDTO reservation, UserDTO customer, String email) {
    	try{
    		try{
    			Reservation r = this.em.find(Reservation.class, reservation.getReservationId());

    			if (r == null)
    				throw new ReservationNotFoundException();

    			Customer c = this.em.find(Customer.class, customer.getUsername());
    			if (c == null)
    				throw new Exception();

    			this.utx.begin();

    			CustomerInvites invite = new CustomerInvites();
    			invite.setReservation(r);
    			invite.setCustomer(c);
    			invite.setFriendAddress(email);
    			String link = String.valueOf(invite.hashCode());
    			invite.setLink(link);
    			this.em.persist(invite);
    			this.utx.commit();
    			
    			return link;
    		} catch (RuntimeException e){
    			this.utx.rollback();
    			this.log.severe("ERROR: Sending invitation link: " + e.getMessage());
    			throw new RuntimeException();
    		}
    	} catch (Exception e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
    }

	/**
     * @see BookingManagerInterface#getAvailableExcursions(LocationDTO)
     */
    @Override
    @SuppressWarnings("unchecked")
    @RolesAllowed({Customer.role})
    public List<ExcursionDTO> getAvailableExcursions(LocationDTO location, Date fromDate, Date toDate) {
        try{
        	this.utx.begin();
        	try{
	        	Location l = this.em.find(Location.class, location.getLocationId());
	    		if (l == null){
	    			this.log.severe("ERROR: Location " + location.getName() + " not found");
	    			this.utx.rollback();
	    			throw new LocationNotFoundException();
	    		}
	    		
	    		List<Excursion> products = null;
	    		Calendar since = Calendar.getInstance();
	    		since.setTime(fromDate);
	    		Calendar until = Calendar.getInstance();
	    		until.setTime(toDate);
	    		// checking if excursion's weekday is included in travel
	    		if (CalendarUtils.daysBetween(since, until) < 7){
	    			products = 
	                		this.em.createNamedQuery("getAvailableExcursionBetweenDates").
	                		setParameter("location", l).
	                		setParameter("since", since).
	                		setParameter("until", until).
	                		getResultList();
	    		} else {
	    			products = 
	            		this.em.createNamedQuery("getAllExcursionsInLocation").
	            		setParameter("location", l).getResultList();
	    		}
	    		
	    		this.utx.commit();
	    		ArrayList<ExcursionDTO> dto = new ArrayList<ExcursionDTO>();
	    		for (Excursion e : products){
	    			dto.add(e.toDTO());
	    		}
	    		return dto;
	        } catch (RuntimeException e){
	        	this.utx.rollback();
				this.log.severe("ERROR: Sending invitation link: " + e.getMessage());
				throw new RuntimeException();
			}
        } catch (Exception e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
			throw new RuntimeException();
    	}
    }

	/**
     * @see BookingManagerInterface#getAvailableLeavingDates(TransportMeanDTO, Date, Date, int, int)
     */
    @SuppressWarnings("unchecked")
	@Override
	@RolesAllowed({Customer.role})
    public List<Date> getAvailableLeavingDates(TransportMeanDTO mean, Date fromDate, Date toDate, int duration, int travellers) 
    		throws ProductNotFoundException{
    	try{
    		this.utx.begin();
    		try{
    			TransportMean m = this.em.find(TransportMean.class, mean.getProductId());
    			if (m == null){
    				this.log.severe("ERROR: Mean " + mean.getProductId() + " not found");
    				this.utx.rollback();
    				throw new ProductNotFoundException();
    			}
    			TransportMean back = (TransportMean) this.em.createNamedQuery("getExistingBackMeanOfTypeToLocation").
    					setParameter("type", this.em.find(MeanType.class, Long.decode(MeanType.BACK_FLIGHT))).
    					setParameter("location", m.getLocation()).
    					getSingleResult();
    			if (back == null){
    				this.log.severe("ERROR: Comingback for mean " + mean.getProductId() + " not found");
    				this.utx.rollback();
    				throw new ProductNotFoundException();
    			}

    			// setting days interval
    			Calendar searchSince = Calendar.getInstance();
    			searchSince.setTime(fromDate);
    			Calendar searchUntil = Calendar.getInstance();
    			searchUntil.setTime(toDate);
    			long interval = CalendarUtils.daysBetween(searchSince, searchUntil);
    			if (interval > 15){
    				searchUntil.setTime(fromDate);
    				searchUntil.add(Calendar.DATE, 15);
    			}
    			
    			// for each day, need to fetch available going-comingback means
    			Calendar current = Calendar.getInstance();
    			List<BookedMeanDescriptor> bookings;
    			List<Date> availableDates = new ArrayList<>();
    			Query going = this.em.createNamedQuery("getLeavingMeanAvailabilityInDate").
    					setParameter(1, m.getProductId());
    			Query comingBack = this.em.createNamedQuery("getComingBackMeanAvailabilityInDate").
    					setParameter(1, m.getProductId());
    			JavaBeanResult.setQueryResultClass(going, BookedMeanDescriptor.class);
    			JavaBeanResult.setQueryResultClass(comingBack, BookedMeanDescriptor.class);
    			
    			for (int i = 0;i <= interval;i++){
    				searchSince.add(Calendar.DATE, (i == 0) ? 0 : 1);
    				current.setTime(searchSince.getTime());
    				current.add(Calendar.DATE, duration);

    				bookings = going.setParameter(2, searchSince.getTime()).
    						getResultList();
    				if (bookings.size() > 1){
    					this.log.severe("Query getLeavingMeanAvailabilityInDate should return 1 row");
    					throw new RuntimeException();
    				}
    				
    				if (bookings.size() != 0){
    					if (bookings.get(0).getAvailability() < travellers) // sold out
        					continue;
    				}

    				bookings = comingBack.setParameter(2, current.getTime()).
    						getResultList();
    				if (bookings.size() > 1){
    					this.log.severe("Query getComingBackMeanAvailabilityInDate should return 1 row");
    					throw new RuntimeException();
    				}
    				
    				if (bookings.size() == 0){
    					availableDates.add(searchSince.getTime());
    					continue;
    				}
    				if (bookings.get(0).getAvailability() < travellers) // sold out
    					continue;

    				// there still are enough spaces
    				availableDates.add(searchSince.getTime());
    			}
    			this.utx.commit();
    			return availableDates;
    		} catch (RuntimeException | RollbackException | HeuristicMixedException | HeuristicRollbackException e){
    			this.utx.rollback();
    			this.log.severe("ERROR: Getting available leaving dates: " + e.getMessage());
    			throw new RuntimeException();
    		} 
    	} catch (SystemException | NotSupportedException e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
    }

	/**
     * @see BookingManagerInterface#getReservation(ReservationDTO)
     */
    @RolesAllowed({Customer.role})
    @Override
    public ReservationDTO getReservation(ReservationDTO reservation) 
    		throws ReservationNotFoundException{
    	try{
    		this.utx.begin();
    		try{
    			Reservation r = this.em.find(Reservation.class, reservation.getReservationId());

    			if (r == null){
    				this.log.severe("ERROR: Reservation " + reservation.getReservationId() + " not found");
    				this.utx.rollback();
    				throw new ReservationNotFoundException();
    			}

    			Customer c = this.em.find(Customer.class, this.context.getCallerPrincipal().getName());
    			this.utx.commit();
    			return r.toDTO(c.toDTO());
    		} catch (RuntimeException e){
    			this.utx.rollback();
    			this.log.severe("ERROR: Getting reservation: " + e.getMessage());
    			throw new RuntimeException();
    		}
    	} catch (Exception e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
    }

	/**
     * @see BookingManagerInterface#checkAvailability(TransportMeanDTO, HotelDTO, Date, int, int)
     */
    @SuppressWarnings("unchecked")
	@Override
	@RolesAllowed({Customer.role})
    public boolean checkAvailability(TransportMeanDTO mean, HotelDTO hotel, Date leavingDate, Date endDate, 
    		int travellers, Map<HotelRoomType, Integer> rooms) 
    		throws ProductNotFoundException { 
    	try{
    		this.utx.begin();
    		try{
    			TransportMean m = this.em.find(TransportMean.class, mean.getProductId());
    			if (m == null){
    				this.log.severe("ERROR: Mean " + mean.getProductId() + " not found");
    				this.utx.rollback();
    				throw new ProductNotFoundException();
    			}
    			TransportMean back = (TransportMean) this.em.createNamedQuery("getExistingBackMeanOfTypeToLocation").
    					setParameter("type", this.em.find(MeanType.class, Long.decode(MeanType.BACK_FLIGHT))).
    					setParameter("location", m.getLocation()).
    					getSingleResult();
    			if (back == null){
    				this.log.severe("ERROR: Comingback for mean " + mean.getProductId() + " not found");
    				this.utx.rollback();
    				throw new ProductNotFoundException();
    			}

    			Hotel h = this.em.find(Hotel.class, hotel.getProductId());
    			if (h == null){
    				this.log.severe("ERROR: hotel " + hotel.getProductId() + " not found");
    				this.utx.rollback();
    				throw new ProductNotFoundException();
    			}

    			Map<String, Query> queries = 
    					this.prepareAndLockAvailabilityQueries(m, back, h, leavingDate, endDate, travellers);
    			List<BookedMeanDescriptor> bookingsGoing = queries.get("getLeavingMeanAvailabilityInDate").getResultList();
    			List<BookedMeanDescriptor> bookingsComingBack = queries.get("getComingBackMeanAvailabilityInDate").getResultList();
    			List<BookedRoomDescriptor> bookingsHotel = queries.get("getHotelBookedRoomsBetweenDates").getResultList();
    			
    			this.utx.commit();
    			// checking mean
    			if (bookingsGoing.size() > 0)
    				if (bookingsGoing.get(0).getAvailability() < travellers)
    					return false;
    			
    			if (bookingsComingBack.size() > 0)
    				if (bookingsComingBack.get(0).getAvailability() < travellers)
    					return false;

    			// checking hotel
    			// not enough total rooms?
    			for (HotelRoomType t : rooms.keySet()){
    				if (rooms.get(t) != null){
	    				if (rooms.get(t) > 
	    					h.getHotelRooms().get(t).getRooms())
	    					return false;
    				}
    			}

    			// not enough available rooms?
    			for (BookedRoomDescriptor d : bookingsHotel){
    				if (rooms.get(d.getType()) != null){
	    				if (d.getTotalRooms() - d.getBookedRooms() <
	    						rooms.get(d.getType()))
	    					return false;
    				}
    			}
    			
    			return true;
    		} catch (RuntimeException | RollbackException | HeuristicMixedException | HeuristicRollbackException e){
    			this.log.severe("ERROR: Checking availability: " + e.getMessage());
    			this.utx.rollback();
    			throw new RuntimeException();
    		}
    	}catch (SystemException | NotSupportedException e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
    }
    
    /**
     * This method creates, sets properties and returns the queries needed
     * in order to check availability of a reservation.
     * It doesn't handle exceptions
     * It garantees to: create queries, set parameters and set pessimistic locking
     * @param mean
     * @param hotel
     * @param leavingDate
     * @param duration
     * @param travellers
     * @return
     */
    private Map<String, Query> prepareAndLockAvailabilityQueries(TransportMean mean, TransportMean back, Hotel hotel, Date leavingDate, Date endDate, int travellers){
    	try{
	    	if (this.utx.getStatus() == Status.STATUS_NO_TRANSACTION)
	    		throw new RuntimeException();

	    	Calendar leaving = Calendar.getInstance();
	    	Calendar comingBack = Calendar.getInstance();
	    	leaving.setTime(leavingDate);
	    	comingBack.setTime(endDate);
	    	Query qGoing = this.em.createNamedQuery("getLeavingMeanAvailabilityInDate").
	    			setParameter(1, mean.getProductId()).
	    			setParameter(2, leaving.getTime());
	    	
	    	Query qComingBack = this.em.createNamedQuery("getComingBackMeanAvailabilityInDate").
	    			setParameter(1, back.getProductId()).
	    			setParameter(2, comingBack.getTime());
	    	Query qHotel = this.em.createNamedQuery("getHotelBookedRoomsBetweenDates").
	    			setParameter(1, hotel.getProductId()).
	    			setParameter(2, leaving.getTime()).
	    			setParameter(3, comingBack.getTime());
	    	JavaBeanResult.setQueryResultClass(qGoing, BookedMeanDescriptor.class);
	    	JavaBeanResult.setQueryResultClass(qComingBack, BookedMeanDescriptor.class);
	    	JavaBeanResult.setQueryResultClass(qHotel, BookedRoomDescriptor.class);
	    	
//	    	qGoing.setHint(QueryHints.PESSIMISTIC_LOCK,
//	                           PessimisticLock.Lock);
//	    	qComingBack.setHint(QueryHints.PESSIMISTIC_LOCK,
//	                           PessimisticLock.Lock);
//	    	qHotel.setHint(QueryHints.PESSIMISTIC_LOCK,
//	                           PessimisticLock.Lock);

	    	HashMap<String, Query> queries = new HashMap<>();
	    	queries.put("getLeavingMeanAvailabilityInDate", qGoing);
	    	queries.put("getComingBackMeanAvailabilityInDate", qComingBack);
	    	queries.put("getHotelBookedRoomsBetweenDates", qHotel);
	    	return queries;
    	} catch (Exception e){
			throw new RuntimeException(e);
		}
    }

	/**
     * @see BookingManagerInterface#book(TransportMeanDTO, HotelDTO, List<ExcursionDTO>, UserDTO, Date, int, int)
     */
    @SuppressWarnings("unchecked")
	@Override
	@RolesAllowed({Customer.role})
    public ReservationDTO book(ReservationDTO reservation, boolean withDiscount) 
    		throws ProductNotFoundException, ProductSoldOutException, InvalidReservationException {
    	/*
    	 * Funzionamento del locking
    	 * si apre una transazione con em.getTransaction().begin()
    	 * si preparano le query con 
    	 * q.setHint(EclipseLinkQueryHints.PESSIMISTIC_LOCK,
                           PessimisticLock.Lock);
                           
           I write concorrenti sono bloccati fino a em.getTransaction().commit()
    	 */
    	try{
    		this.utx.begin();
    		try{
    			// Retrieving products
    			TransportMean mean = this.em.find(TransportMean.class, reservation.getMean().getProductId());
    			if (mean == null){
    				this.log.severe("ERROR: Mean " + reservation.getMean().getProductId() + " not found");
    				throw new ProductNotFoundException();
    			}
    			TransportMean back = (TransportMean) this.em.createNamedQuery("getExistingBackMeanOfTypeToLocation").
    					setParameter("type", this.em.find(MeanType.class, Long.decode(MeanType.BACK_FLIGHT))).
    					setParameter("location", mean.getLocation()).
    					getSingleResult();
    			if (back == null){
    				this.log.severe("ERROR: Comingback for mean " + mean.getProductId() + " not found");
    				throw new ProductNotFoundException();
    			}

    			Hotel hotel = this.em.find(Hotel.class, reservation.getHotel().getProductId());
    			if (hotel == null){
    				this.log.severe("ERROR: hotel " + reservation.getHotel().getProductId() + " not found");
    				throw new ProductNotFoundException();
    			}

    			// Checking availability
    			Map<String, Query> queries = 
    					this.prepareAndLockAvailabilityQueries(
    							mean, back, hotel, reservation.getLeavingDate(), reservation.getEndDate(), reservation.getTravellers());
    			List<BookedMeanDescriptor> bookingsGoing = queries.get("getLeavingMeanAvailabilityInDate").getResultList();
    			List<BookedMeanDescriptor> bookingsComingBack = queries.get("getComingBackMeanAvailabilityInDate").getResultList();
    			List<BookedRoomDescriptor> bookingsHotel = queries.get("getHotelBookedRoomsBetweenDates").getResultList();
    			
    			// checking mean
    			if (bookingsGoing.size() > 0)
    				if (bookingsGoing.get(0).getAvailability() < reservation.getTravellers())
    					throw new ProductSoldOutException();
    				
    			if (bookingsComingBack.size() > 0)
    				if (bookingsComingBack.get(0).getAvailability() < reservation.getTravellers())
    					throw new ProductSoldOutException();
    			
    			// checking hotel
    			for (BookedRoomDescriptor d : bookingsHotel){
    				if (reservation.getHotelRooms().get(d.getType()) != null && 
    					((d.getTotalRooms() - d.getBookedRooms()) < reservation.getHotelRooms().get(d.getType())))
						throw new ProductSoldOutException();
    			}
    			
    			// Book travel
    			BookedMean bookedGoing = new BookedMean();
    			BookedMean bookedBack = new BookedMean();
    			bookedGoing.setProduct(mean);
    			bookedGoing.setQuantity(reservation.getTravellers());
    			bookedBack.setProduct(back);
    			bookedBack.setQuantity(reservation.getTravellers());
    			
    			BookedHotel bookedHotel = new BookedHotel();
    			bookedHotel.setProduct(hotel);
    			bookedHotel.setBookedRooms(reservation.getHotelRooms());
    			
    			List<BookedExcursion> bexs = new ArrayList<>();
    	        for (ExcursionDTO e : reservation.getExcursions()){
    	        	BookedExcursion be = new BookedExcursion();
    	        	Excursion ex = this.em.find(Excursion.class, e.getProductId());
    	        	if (ex == null)
    	        		throw new ProductNotFoundException();
    	        	
    	        	be.setProduct(ex);
    	        	bexs.add(be);
    	        }
    	        
    	        Customer c = this.em.find(Customer.class, this.context.getCallerPrincipal().getName());
    	        Reservation r = new Reservation();
    	        r.setExcursions(bexs);
    	        r.setGoingMean(bookedGoing);
    	        r.setComingBackMean(bookedBack);
    	        r.setHotel(bookedHotel);
    	        r.setLeavingDate(reservation.getLeavingDate());
    	        r.setEndDate(reservation.getEndDate());
    	        r.setCustomer(c);

    	        Calendar l = Calendar.getInstance();
    	        Calendar e = Calendar.getInstance();
    	        l.setTime(reservation.getLeavingDate());
    	        e.setTime(reservation.getEndDate());
    	        int duration = (int) CalendarUtils.daysBetween(l, e);
    	        
    	        BigDecimal totalPrice = this.getTotalPrice(reservation.getMean(), reservation.getTravellers(), duration,
    	        		reservation.getHotel(), reservation.getHotelRooms(), reservation.getExcursions(), withDiscount);
    	        r.setTotalPrice(totalPrice);
    	        
    	        if (withDiscount){
    	        	c.setDiscountPercentage(0);
    	        	this.em.merge(c);
    	        }

    	        this.em.persist(r);
    	        this.utx.commit();
				
    	        return r.toDTO(c.toDTO());
    		} catch (ProductNotFoundException | ProductSoldOutException e){
    			this.log.severe("ERROR " + e.getClass().getName() + ": Booking: " + e.getMessage());
    			this.utx.rollback();
    			throw e;
    		} catch (RuntimeException e){
    			this.log.severe("ERROR: Booking: " + e.getMessage());
    			this.utx.rollback();
    			throw new InvalidReservationException();
    		} catch (RollbackException | HeuristicMixedException | HeuristicRollbackException e){
    			this.log.severe("ERROR " + e.getClass().getName() + ": Booking: " + e.getMessage());
				this.utx.rollback();
				throw new RuntimeException();
			}
    	} catch (SystemException | NotSupportedException e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
    	
			
    }

	@SuppressWarnings("unchecked")
	@Override
	@RolesAllowed({Customer.role})
	public Map<HotelRoomType, Integer> getAvailableHotelRooms(HotelDTO hotel,
			Date fromDate, Date toDate) throws ProductNotFoundException {
		try{
    		this.utx.begin();
    		try{
    			Map<HotelRoomType, Integer> rooms = new HashMap<>();
    			Hotel h = this.em.find(Hotel.class, hotel.getProductId());
    			if (h == null){
    				this.log.severe("ERROR: hotel " + hotel.getProductId() + " not found");
    				this.utx.rollback();
    				throw new ProductNotFoundException();
    			}
    			
    			Query query = this.em.createNamedQuery("getHotelBookedRoomsBetweenDates").
    					setParameter(1, h.getProductId()).
    	    			setParameter(2, fromDate).
    	    			setParameter(3, toDate);
    			List<BookedRoomDescriptor> bookingsHotel = query.getResultList();
    			
    			for (BookedRoomDescriptor d : bookingsHotel){
    				if (d.getTotalRooms() > d.getBookedRooms())
    					rooms.put(d.getType(), d.getTotalRooms() - d.getBookedRooms());
    			}
    			
    			this.utx.commit();
    			return rooms;
    		} catch (ProductNotFoundException e){
    			this.log.severe("ERROR " + e.getClass().getName() + ": Getting available rooms: " + e.getMessage());
    			this.utx.rollback();
    			throw e;
    		}catch (Exception e){
    			this.log.severe("ERROR: Getting available rooms: " + e.getMessage());
    			this.utx.rollback();
    			throw new RuntimeException();
    		}
		} catch (SystemException | NotSupportedException e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
	}

	@Override
	public BigDecimal getTotalPrice(TransportMeanDTO mean, int travellers, int duration,
			HotelDTO hotel, Map<HotelRoomType, Integer> hotelRooms,
			List<ExcursionDTO> excursions, boolean withDiscount)
					throws ProductNotFoundException {
		try{
			TransportMean m = this.em.find(TransportMean.class, mean.getProductId());
			if (m == null){
				this.log.severe("ERROR: mean " + mean.getProductId() + " not found");
				throw new ProductNotFoundException();
			}

			Hotel h = this.em.find(Hotel.class, hotel.getProductId());
			if (h == null){
				this.log.severe("ERROR: hotel " + hotel.getProductId() + " not found");
				throw new ProductNotFoundException();
			}

			List<Excursion> exs = new ArrayList<>();
			for (ExcursionDTO ex : excursions){
				Excursion e = this.em.find(Excursion.class, ex.getProductId());
				if (e == null){
					this.log.severe("ERROR: excursion " + ex.getProductId() + " not found");
					throw new ProductNotFoundException();
				}
				exs.add(e);
			}

			Customer c = this.em.find(Customer.class, this.context.getCallerPrincipal().getName());
			BigDecimal totalPrice = mean.getPrice();
			totalPrice = totalPrice.multiply(new BigDecimal(travellers));
			for (HotelRoomType t : hotelRooms.keySet()){
				if (hotelRooms.get(t) != null){
					BigDecimal roomPrice = hotel.getPrices().get(t);
					// formula: totalPrice += (roomPrice * roomNumber) * duration
					BigDecimal hotelPrice = roomPrice.multiply(new BigDecimal(hotelRooms.get(t)));
					hotelPrice = hotelPrice.multiply(new BigDecimal(duration));
					totalPrice = totalPrice.add(hotelPrice);
				}
			}
			for (Excursion e : exs){
				totalPrice = totalPrice.add(e.getPrice().multiply(new BigDecimal(travellers)));
			}

			if (withDiscount && c != null && c.getDiscountPercentage() != 0){
				BigDecimal discount = new BigDecimal(c.getDiscountPercentage() / 100.0);
				totalPrice = totalPrice.multiply(discount);
			}

			return totalPrice;
		} catch (RuntimeException e){
			this.log.severe("ERROR: Computing total price " + e.getMessage());
			throw new RuntimeException();
		}
	}

	@Override
	public ReservationDTO getReservationByLink(String invitationLink)
			throws ReservationNotFoundException {
		try{
			try{
				this.utx.begin();
				CustomerInvites invite = (CustomerInvites) this.em.createNamedQuery("getInvitesByLink").
						setParameter("link", invitationLink).getSingleResult();

				if (invite == null){    				
					this.utx.rollback();
					throw new ReservationNotFoundException();
				}
				
				this.utx.commit();
				return invite.getReservation().toDTO(invite.getCustomer().toDTO());
			} catch (RuntimeException | RollbackException | HeuristicMixedException | HeuristicRollbackException e){
				this.utx.rollback();
				this.log.severe("ERROR: Processing invitation link: " + e.getMessage());
	    		throw new ReservationNotFoundException();
			}
		} catch (SystemException | NotSupportedException e){
    		this.log.severe("ERROR: User transaction exception: " + e.getMessage());
    		throw new RuntimeException();
    	}
		
	}

}
